package demo.retry;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;

/**
 * 
 *
 * @author haijun
 * @version 1.0.0
 * @email "mailto:iamxiaohaijun@gmail.com"
 * @date 2023.07.19 19:14
 * @since 2023.1.1
 */
@Slf4j
public final class ApiInvokeAndRetry {
    /**
     * Api invoke and retry
     *
     * @since 2023.1.1
     */
    private ApiInvokeAndRetry() {

    }

    /**
     * Retry
     *
     * @param <T>      parameter
     * @param callable callable
     * @return the t
     * @since 2023.1.1
     */
    public static <T> T retry(CheckedCallable<T> callable) {
        return retry(callable, 3000L);
    }

    /**
     * Retry
     *
     * @param <T>      parameter
     * @param callable callable
     * @param waitTime wait time
     * @return the t
     * @since 2023.1.1
     */
    public static <T> T retry(CheckedCallable<T> callable, Long waitTime) {
        return retry(callable, waitTime, 0);
    }

    /**
     * Retry
     *
     * @param <T>        parameter
     * @param callable   callable
     * @param waitTime   wait time
     * @param retryTimes retry times
     * @return the t
     * @since 2023.1.1
     */
    public static <T> T retry(CheckedCallable<T> callable, Long waitTime, Integer retryTimes) {
        return retry(callable, waitTime, retryTimes, 0L);
    }

    /**
     * Retry
     *
     * @param <T>         parameter
     * @param callable    callable
     * @param waitTime    wait time
     * @param retryTimes  retry times
     * @param timeOutTime time out time
     * @return the t
     * @since 2023.1.1
     */
    public static <T> T retry(CheckedCallable<T> callable, Long waitTime, Integer retryTimes, Long timeOutTime) {
        return retry(callable, waitTime, retryTimes, timeOutTime, TimeUnit.MILLISECONDS);
    }

    /**
     * Retry
     *
     * @param <T>         parameter
     * @param callable    callable
     * @param waitTime    wait time
     * @param retryTimes  retry times
     * @param timeOutTime time out time
     * @param timeUnit    time unit
     * @return the t
     * @since 2023.1.1
     */
    public static <T> T retry(CheckedCallable<T> callable, Long waitTime, Integer retryTimes, Long timeOutTime, TimeUnit timeUnit) {
        return retry(callable, waitTime, retryTimes, timeOutTime, timeUnit, Exception.class);
    }

    /**
     * Retry
     *
     * @param <T>         parameter
     * @param callable    callable
     * @param waitTimes   wait times
     * @param timeOutTime time out time
     * @param timeUnit    time unit
     * @return the t
     * @since 2023.1.1
     */
    public static <T> T retry(CheckedCallable<T> callable, Long[] waitTimes, Long timeOutTime, TimeUnit timeUnit) {
        Objects.requireNonNull(callable);

        return new Builder<T>()
            .retryTimes(waitTimes.length)
            .timeOutTime(timeOutTime)
            .timeUnit(timeUnit)
            .exceptionClass(Exception.class)
            .builder(() -> {
                    RetryerBuilder<T> retryerBuilder = RetryerBuilder.<T>newBuilder()
                        //retryIf 重试条件
                        .retryIfException()
                        .retryIfRuntimeException()
                        .retryIfExceptionOfType(Exception.class)
                        //等待策略: 每次请求间隔时间
                        .withWaitStrategy(new ProgressiveWaitStrategy(waitTimes, timeUnit))
                        //停止策略 : 尝试请求次数
                        .withStopStrategy(StopStrategies.stopAfterAttempt(waitTimes.length + 1));
                    if (timeOutTime <= 0) {
                        retryerBuilder = retryerBuilder
                            .withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit());
                    } else {
                        ExecutorService executorService = new ThreadPoolExecutor(1,
                            1,
                            0L,
                            TimeUnit.MILLISECONDS,
                            new LinkedBlockingQueue<>(),
                            new ThreadFactoryBuilder().setNameFormat("retry-pool")
                                .build());
                        retryerBuilder = retryerBuilder
                            //时间限制 : 某次请求不得超过指定时间 , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
                            .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(timeOutTime, TimeUnit.MILLISECONDS,
                                executorService));
                    }
                    return retryerBuilder.build();
                }
            )
            .call(callable);

    }

    /**
     * Retry
     *
     * @param <T>            parameter
     * @param callable       callable
     * @param waitTime       wait time
     * @param retryTimes     retry times
     * @param timeOutTime    time out time
     * @param timeUnit       time unit
     * @param exceptionClass exception class
     * @return the t
     * @since 2023.1.1
     */
    @SuppressWarnings("checkstyle:ParameterNumber")
    public static <T> T retry(CheckedCallable<T> callable,
                              Long waitTime,
                              Integer retryTimes,
                              Long timeOutTime,
                              TimeUnit timeUnit,
                              Class<? extends Exception> exceptionClass) {

        Objects.requireNonNull(callable);

        return new Builder<T>()
            .waitTime(waitTime)
            .retryTimes(retryTimes)
            .timeOutTime(timeOutTime)
            .timeUnit(timeUnit)
            .exceptionClass(exceptionClass)
            .builder()
            .call(callable);
    }

    /**
     *
     *
     * @param <T> parameter
     * @author haijun
     * @version 1.0.0
     * @email "mailto:iamxiaohaijun@gmail.com"
     * @date 2023.07.19 19:14
     * @since 2023.1.1
     */
    private static class Builder<T> {
        /**
         * Wait time
         */
        private Long waitTime = 3000L;
        /**
         * Retry times
         */
        private Integer retryTimes = 0;
        /**
         * Time out time
         */
        private Long timeOutTime = 0L;
        /**
         * Time unit
         */
        private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
        /**
         * Exception class
         */
        private Class<? extends Exception> exceptionClass = Exception.class;
        /**
         * Retryer
         */
        private Retryer<T> retryer;

        /**
         * Wait time
         *
         * @param waitTime wait time
         * @return the builder
         * @since 2023.1.1
         */
        public Builder<T> waitTime(Long waitTime) {
            if (waitTime != null && waitTime > 0) {
                this.waitTime = waitTime;
            }
            return this;
        }

        /**
         * Retry times
         *
         * @param retryTimes retry times
         * @return the builder
         * @since 2023.1.1
         */
        public Builder<T> retryTimes(Integer retryTimes) {
            if (retryTimes != null && retryTimes > 0) {
                this.retryTimes = retryTimes;
            }
            return this;
        }

        /**
         * Time out time
         *
         * @param timeOutTime time out time
         * @return the builder
         * @since 2023.1.1
         */
        public Builder<T> timeOutTime(Long timeOutTime) {
            if (timeOutTime != null && timeOutTime > 0) {
                this.timeOutTime = timeOutTime;
            }
            return this;
        }

        /**
         * Time unit
         *
         * @param timeUnit time unit
         * @return the builder
         * @since 2023.1.1
         */
        public Builder<T> timeUnit(TimeUnit timeUnit) {
            if (timeUnit != null) {
                this.timeUnit = timeUnit;
            }
            return this;
        }

        /**
         * Exception class
         *
         * @param exceptionClass exception class
         * @return the builder
         * @since 2023.1.1
         */
        public Builder<T> exceptionClass(Class<? extends Exception> exceptionClass) {
            if (this.timeUnit != null) {
                this.exceptionClass = exceptionClass;
            }
            return this;
        }

        /**
         * Builder
         *
         * @return the builder
         * @since 2023.1.1
         */
        public Builder<T> builder() {
            RetryerBuilder<T> retryerBuilder = RetryerBuilder.<T>newBuilder()
                //retryIf 重试条件
                .retryIfException()
                .retryIfRuntimeException()
                .retryIfExceptionOfType(this.exceptionClass)
                //等待策略: 每次请求间隔时间
                .withWaitStrategy(WaitStrategies.fixedWait(this.waitTime, this.timeUnit))
                //停止策略 : 尝试请求次数
                .withStopStrategy(StopStrategies.stopAfterAttempt(this.retryTimes + 1));

            if (this.timeOutTime <= 0) {
                retryerBuilder = retryerBuilder
                    .withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit());
            } else {
                ExecutorService executorService = new ThreadPoolExecutor(1,
                    1,
                    0L,
                    TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>(),
                    new ThreadFactoryBuilder().setNameFormat("retry-pool")
                        .build());
                retryerBuilder = retryerBuilder
                    //时间限制 : 某次请求不得超过指定时间 , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
                    .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(this.timeOutTime, this.timeUnit, executorService));
            }
            this.retryer = retryerBuilder.build();

            return this;
        }

        /**
         * Builder
         *
         * @param supplier supplier
         * @return the builder
         * @since 2023.1.1
         */
        public Builder<T> builder(Supplier<Retryer<T>> supplier) {
            this.retryer = supplier.get();
            this.retryer = Optional.ofNullable(retryer).orElse(builder().retryer);
            return this;
        }

        /**
         * Call
         *
         * @param callable callable
         * @return the t
         * @since 2023.1.1
         */
        public T call(CheckedCallable<T> callable) {
            try {
                return this.retryer.call(() -> {
                    try {
                        return callable.call();
                    } catch (Throwable throwable) {
                        throw new RuntimeException(throwable);
                    }
                });
            } catch (ExecutionException | RetryException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @FunctionalInterface
    public interface CheckedCallable<T> {

        /**
         * Call
         *
         * @return the t
         * @throws Throwable throwable
         * @since 2023.1.1
         */
        @Nullable
        T call() throws Throwable;
    }
}
